import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.zip.GZIPOutputStream;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;


public class ArtCollection {
	public static final String DEFAULT_ART_FILE = "config/catalog.xml";
	public static final String DEFAULT_TRAINING_FILE = "config/train.txt";
	//The number of doubles used to describe the image content of each artwork
	public final static int IMAGE_SUMMARY_LENGTH = 225;
	public static final FeatureExtractionMethod methods[] =
		{new ClusteringMethod(), new ResolutionReductionMethod()};
	FeatureExtractionMethod m_method;
	
	public enum Method {
		CLUSTERING(0), RESOLUTION(1);
	    private final int index;
	    Method(int index) { this.index = index; }
	    public int getIndex() { return index; }
	}
	
	ArrayList<ArtPiece> m_artPieces;
	ArrayList<String> m_authors;
	
	ArtCollection()
	{
		m_method = methods[Method.CLUSTERING.getIndex()];
		m_artPieces = new ArrayList<ArtPiece>();
		m_authors = new ArrayList<String>();
		loadArt(DEFAULT_ART_FILE);
	}
	
	ArtCollection(Method chosenMethod)
	{
		m_method = methods[chosenMethod.getIndex()];
		m_artPieces = new ArrayList<ArtPiece>();
		m_authors = new ArrayList<String>();
		loadArt(DEFAULT_ART_FILE);
	}
	
	ArtCollection(Method chosenMethod, String artFile)
	{
		m_method = methods[chosenMethod.getIndex()];
		m_artPieces = new ArrayList<ArtPiece>();
		m_authors = new ArrayList<String>();
		loadArt(artFile);
	}
	
	public void loadArt(String filename)
	{
		try
		{
			File fXmlFile = new File(filename);
			DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
			Document doc = dBuilder.parse(fXmlFile);
			doc.getDocumentElement().normalize();
			NodeList paintingNodes = doc.getElementsByTagName("painting");
			for (int i = 0; i < paintingNodes.getLength(); ++i)
			{
				Node node = paintingNodes.item(i);
				if (node.getNodeType() == Node.ELEMENT_NODE)
				{
					ArtPiece piece = new ArtPiece();
					Element paintingElement = (Element) node;
					piece.author = getTagValue("author", paintingElement);
					piece.title = getTagValue("title", paintingElement);
					piece.sampleUrls = getTagValues("url", paintingElement);
					m_artPieces.add(piece);
					if (!m_authors.contains(piece.author))
					{
						m_authors.add(piece.author);
					}
				}
			}
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}
	
	private ArrayList<Double> neuralNetAnswer(int authorIndex, int pieceIndex)
	{
		ArrayList<Boolean> data = new ArrayList<Boolean>();
		ArrayList<Boolean> hamCode = new ArrayList<Boolean>();
		ArrayList<Double> result = new ArrayList<Double>();
		int maxAuthorIndex = m_authors.size() - 1;
		int maxPieceIndex = m_artPieces.size() - 1;
		while (maxAuthorIndex != 0)
		{
			data.add((authorIndex & 1) != 0);
			maxAuthorIndex = maxAuthorIndex >> 1;
			authorIndex = authorIndex >> 1;
		}
		while (maxPieceIndex != 0)
		{
			data.add((pieceIndex & 1) != 0);
			maxPieceIndex = maxPieceIndex >> 1;
			pieceIndex = pieceIndex >> 1;
		}
		
		//Fill the data bits of our Hamming code
		int dataIndex = 0;
		int nextParityPosition = 1;
		while (dataIndex < data.size())
		{
			if ((hamCode.size() + 1) == nextParityPosition)
			{
				hamCode.add(false);
				nextParityPosition = nextParityPosition << 1;
			}
			else
			{
				hamCode.add(data.get(dataIndex));
				dataIndex += 1;
			}
		}

		//Calculate the parity bits
		for (int i = 1; i <= hamCode.size(); i = i << 1)
		{
			for (int j = 1; j <= hamCode.size(); ++j)
			{
				if (i != j && (i & j) != 0 && hamCode.get(j - 1))
				{
					hamCode.set(i-1, !hamCode.get(i-1));
				}
			}
		}
		
		//Convert the Hamming code to doubles (0.0 or 1.0)
		for (int i = 0; i < hamCode.size(); ++i)
		{
			result.add(hamCode.get(i) ? 1.0 : 0.0);
		}
		
		return result;
	}
	
	public void createTrainingFile(String filename)
	{
		try {
			FileOutputStream fileStream = new FileOutputStream(filename);
			GZIPOutputStream compressedStream = new GZIPOutputStream(fileStream);
			ObjectOutputStream outStream = new ObjectOutputStream(compressedStream);
			
			for (int i = 0; i < m_artPieces.size(); ++i)
			{
				ArtPiece piece = m_artPieces.get(i);
				int authorIndex = m_authors.indexOf(piece.author);
				//Initialize the expected neural network expected answer to all 0
				ArrayList<Double> neuralNetAnswer = neuralNetAnswer(authorIndex, i);
				
				//Generate the neural network input
				for (BufferedImage image : piece.getSampleImages())
				{
					if (image != null)
					{
						ArrayList<Double> features = m_method.process(image, IMAGE_SUMMARY_LENGTH);
						outStream.writeObject(neuralNetAnswer);
						outStream.writeObject(features);
					}
				}
			}
			outStream.close();
			fileStream.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public void createTrainingFile()
	{
		createTrainingFile(DEFAULT_TRAINING_FILE);
	}
	
	//Return the number of bits used to identify each piece of art 
	public int getNeuralNetOutputLength()
	{
		return neuralNetAnswer(0, 0).size();
	}
	
	//Returns the corrected data part of a hamming code
	private ArrayList<Boolean> decodeHamming(ArrayList<Boolean> list)
	{
		ArrayList<Boolean> result = new ArrayList<Boolean>();
		ArrayList<Boolean> input = new ArrayList<Boolean>();
		input.addAll(list);
		//error correction
		int error = 0;
		for (int i = 1; i <= list.size(); ++i)
		{
			if (list.get(i-1))
			{
				error ^= i;
			}
		}
		if (error != 0)
		{
			list.set(error - 1, !list.get(error - 1));
		}

		int nextParityPosition = 1;
		for (int i = 1; i <= list.size(); ++i)
		{
			if (i == nextParityPosition)
			{
				nextParityPosition = nextParityPosition << 1;
			}
			else
			{
				result.add(input.get(i - 1));
			}
		}
		return result;
	}
	
	/**Takes the output from a neural network and returns the predicted piece,
	 * which includes information about its author, type, and school
	 * @param neuralNetOutput
	 * @return The predicted piece and all of the information about that piece
	 */
	public ArtPiece getPiecePrediction(ArrayList<Double> neuralNetOutput)
	{
		ArrayList<Boolean> hamming = new ArrayList<Boolean>();
		for (int i = 0; i < neuralNetOutput.size(); ++i)
		{
			hamming.add(neuralNetOutput.get(i) > 0.5);
		}
		ArrayList<Boolean> data = decodeHamming(hamming);
		int authorBits = (int) Math.ceil(Math.log(m_authors.size()) / Math.log(2));
		int pieceIndex = 0;
		for (int i = data.size() - 1; i >= authorBits; --i)
		{
			pieceIndex = pieceIndex << 1;
			pieceIndex += data.get(i) ? 1 : 0;
		}
		return m_artPieces.get(pieceIndex);
	}
	
	/**Takes the output from a neural network and returns the predicted author,
	 * which may be different than the author of the predicted piece
	 * @param neuralNetOutput
	 * @return The name of the author, as predicted by the neural network
	 */
	public String getAuthorPrediction(ArrayList<Double> neuralNetOutput)
	{
		ArrayList<Boolean> hamming = new ArrayList<Boolean>();
		for (int i = 0; i < neuralNetOutput.size(); ++i)
		{
			hamming.add(neuralNetOutput.get(i) > 0.5);
		}
		ArrayList<Boolean> data = decodeHamming(hamming);
		int authorBits = (int) Math.ceil(Math.log(m_authors.size()) / Math.log(2));
		int authorIndex = 0;
		for (int i = authorBits - 1; i >= 0; --i)
		{
			authorIndex = authorIndex << 1;
			authorIndex += data.get(i) ? 1 : 0;
		}
		return m_authors.get(authorIndex);
	}
	
	private Object getPrediction(ArrayList<Double> neuralNetOutput, int offset, ArrayList<?> choices)
	{
		Object bestChoice = new Object();
		double bestScore = -1;
		for (int i = 0; i < choices.size(); ++i)
		{
			if (neuralNetOutput.get(offset + i) > bestScore)
			{
				bestChoice = choices.get(i);
			}
		}
		return bestChoice;		
	}
	
	private static String getTagValue(String sTag, Element eElement)
	{
		String result = "Tag not found";
		if (eElement.getElementsByTagName(sTag).getLength() > 0)
		{
			NodeList nlList = eElement.getElementsByTagName(sTag).item(0).getChildNodes();
			Node nValue = (Node) nlList.item(0);
			result = nValue.getNodeValue();
		}
		return result;
	}
	
	private static ArrayList<String> getTagValues(String sTag, Element eElement)
	{
		ArrayList<String> result = new ArrayList<String>();
		if (eElement.getElementsByTagName(sTag).getLength() > 0)
		{
			NodeList matches = eElement.getElementsByTagName(sTag);
			for (int i = 0; i < matches.getLength(); ++i)
			{
				NodeList nlList = matches.item(i).getChildNodes();
				Node nValue = (Node) nlList.item(0);
				result.add(nValue.getNodeValue());
			}
		}
		return result;
	}	
	
	public static void main(String[] args)
	{
		ArtCollection art = new ArtCollection(Method.RESOLUTION);
		System.out.println("Generating Training Data...");
		art.createTrainingFile();
		System.out.println("Done");
	}
}
